/**
 * Copyright 2014-2020  the original author or authors.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.upb.webserver.common.tools;

import cn.hutool.core.collection.CollectionUtil;
import com.upb.webserver.common.enums.RegexPattern;
import com.upb.webserver.common.exception.WebServerException;
import com.upb.webserver.common.pojo.base.BaseResponse;
import com.upb.webserver.common.returncode.ConstantCode;
import com.upb.webserver.common.returncode.RetCode;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * common method.
 */
@Log4j2
public class WebServerTools {

    private static List<Class> VALID_ANNOTATION_NAME = Arrays.asList(NotNull.class, NotBlank.class);
    public static final String TOKEN_HEADER_NAME = "AuthorizationToken";
    private static final String TOKEN_START = "Token";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_TIME_FORMAT_NO_SPACE = "yyyyMMddHHmmss";
    private static final char[] CHARS = {'2', '3', '4', '5', '6', '7', '8', '9', 'a',
            'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's',
            't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
            'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};


    /**
     * 获取指定位数的数字和字母组合的字符串
     *
     * @param length 字符串长度
     */
    public static String randomString(int length) throws NoSuchAlgorithmException {
        if (length > CHARS.length) {
            return null;
        }
        StringBuffer sb = new StringBuffer();
        Random random = new SecureRandom();
        for (int i = 0; i < length; i++) {
            sb.append(CHARS[random.nextInt(CHARS.length)]);
        }
        return sb.toString();
    }


    /**
     * convert hex to localDateTime.
     */
    public static LocalDateTime hex2LocalDateTime(String hexStr) {
        if (StringUtils.isBlank(hexStr)) {
            return null;
        }
        Long timeLong = Long.parseLong(hexStr, 16);
        return timestamp2LocalDateTime(timeLong);
    }

    /**
     * convert timestamp to localDateTime.
     */
    public static LocalDateTime timestamp2LocalDateTime(Long inputTimeStamp) {
        if (inputTimeStamp == null) {
            log.warn("timestamp2LocalDateTime fail. inputTimeStamp is null");
            return null;
        }
        Instant instant = Instant.ofEpochMilli(inputTimeStamp);
        ZoneId zone = ZoneId.systemDefault();
        return LocalDateTime.ofInstant(instant, zone);
//        Timestamp time = new Timestamp(inputTimeStamp);
//        return time.toLocalDateTime();
    }

    /**
     * LocalDateTime to timestamp
     */
    public static Long localDateTime2Timestamp(LocalDateTime inputDateTime) {
        if (inputDateTime == null) {
            log.warn("localDateTime2Timestamp fail. inputDateTime is null");
            return null;
        }
//        Timestamp time = Timestamp.valueOf(inputDateTime);
//        return time.getTime();
        return inputDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
    }

    /**
     * convert String to localDateTime.
     */
    public static LocalDateTime string2LocalDateTime(String time, String format) {
        if (StringUtils.isBlank(time)) {
            log.info("string2LocalDateTime. time is null");
            return null;
        }
        if (StringUtils.isBlank(format)) {
            log.info("string2LocalDateTime. format is null");
            format = DEFAULT_DATE_TIME_FORMAT;
        }
        DateTimeFormatter df = DateTimeFormatter.ofPattern(format);
        return LocalDateTime.parse(time, df);
    }

    /**
     * convert localDateTime to String.
     */
    public static String localDateTime2String(LocalDateTime dateTime, String format) {
        if (dateTime == null) {
            log.warn("localDateTime2String. dateTime is null");
            return null;
        }
        if (StringUtils.isBlank(format)) {
            log.info("localDateTime2String. format is null");
            format = DEFAULT_DATE_TIME_FORMAT;
        }
        DateTimeFormatter df = DateTimeFormatter.ofPattern(format);
        String localTimeStr = df.format(dateTime);
        return localTimeStr;
    }

    /**
     * conver object to java bean.
     */
    public static <T> T object2JavaBean(Object obj, Class<T> clazz) {
        if (obj == null || clazz == null) {
            log.warn("object2JavaBean. obj or clazz null");
            return null;
        }
        String jsonStr = JsonUtils.objToString(obj);

        return JsonUtils.stringToObj(jsonStr, clazz);
    }


    /**
     * encode list by sha.
     */
    public static String shaList(List<String> values) {
        log.info("shaList start. values:{}", JsonUtils.objToString(values));
        // list按字段排序，并转换成字符串
        String list2SortString = list2SortString(values);
        // SHA加密字符串
        String shaStr = shaEncode(list2SortString);
        log.info("shaList end. ShaStr:{}", shaStr);
        return shaStr;
    }

    /**
     * encode String by sha.
     */
    public static String shaEncode(String inStr) {

        byte[] byteArray = new byte[0];
        try {
            byteArray = inStr.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.warn("shaEncode fail:", e);
            return null;
        }
        byte[] hashValue = getHashValue(byteArray);
        if (Objects.isNull(hashValue)) {
            log.warn("hashValue is null");
            return null;
        }
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < hashValue.length; i++) {
            int val = ((int) hashValue[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }


    /**
     * get hash value
     * type: sha256 or sm3
     */
    public static byte[] getHashValue(byte[] byteArray) {
        byte[] hashResult;
//        if(EncryptType.encryptType == 1) {
//            hashResult = Hash.sha3(byteArray);
//            return hashResult;
//        } else {
        MessageDigest sha = null;
        try {
            sha = MessageDigest.getInstance("SHA-256");
            hashResult = sha.digest(byteArray);
            return hashResult;
        } catch (Exception e) {
            log.error("shaEncode getHashValue fail:", e);
            return null;
        }
//        }
    }


    /**
     * sort list and convert to String.
     */
    private static String list2SortString(List<String> values) {
        if (values == null) {
            throw new NullPointerException("values is null");
        }

        values.removeAll(Collections.singleton(null));// remove null
        Collections.sort(values);

        StringBuilder sb = new StringBuilder();
        for (String s : values) {
            sb.append(s);
        }
        return sb.toString();
    }


    /**
     * @param baseUrl
     * @param parameterMap
     * @return
     */
    public static String handleUrlParameters(String baseUrl, Map<String, Object> parameterMap) {
        Assert.hasText(baseUrl, "Error: baseUrl不能为空");
        UriComponentsBuilder builder = UriComponentsBuilder.fromPath(baseUrl);

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        if (CollectionUtil.isNotEmpty(parameterMap)) {
            parameterMap.forEach((key, value) -> {
                if (Objects.nonNull(value)) {
                    params.put(key, Collections.singletonList(String.valueOf(value)));
                }
            });
        }
        return builder.queryParams(params).build().toUriString();
    }


    /**
     * convert list to url param.
     */
    public static String convertUrlParam(List<String> nameList, List<Object> valueList) {
        if (nameList == null || valueList == null || nameList.size() != valueList.size()) {
            log.error("fail convertUrlParm. nameList or valuesList is error");
            return "";
        }
        StringBuilder urlParamB = new StringBuilder("");
        for (int i = 0; i < valueList.size(); i++) {
            Object value = valueList.get(i);
            if (value != null) {
                urlParamB.append("&").append(nameList.get(i)).append("=").append(value);
            }
        }

        if (urlParamB.length() == 0) {
            log.info("fail convertUrlParam. urlParamB length is 0");
            return "";
        }

        String urlParam = urlParamB.toString();
        return urlParam.substring(1);

    }

    /**
     * convert list to map.
     */
    public static Map<String, Object> buidMap(List<String> nameList, List<Object> valueList) {
        if (nameList == null || valueList == null || nameList.size() != valueList.size()) {
            log.error("fail buidMap. nameList or valuesList is error");
            return null;
        }
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < nameList.size(); i++) {
            map.put(nameList.get(i), valueList.get(i));
        }
        return map;
    }

//    /**
//     * check server host.
//     */
//    public static void checkServerHostConnect(String serverHost) {
//        Boolean state;
//        try {
//            InetAddress address = InetAddress.getByName(serverHost);
//            state = address.isReachable(500);
//        } catch (Exception ex) {
//            log.error("fail checkServerHostConnect", ex);
//            throw new WebServerException(ConstantCode.SERVER_CONNECT_FAIL);
//        }
//
//        if (!state) {
//            log.info("host connect state:{}", state);
//            throw new WebServerException(ConstantCode.SERVER_CONNECT_FAIL);
//        }
//    }


//    /**
//     * check host an port.
//     */
//    public static void checkServerConnect(String serverHost, int serverPort) {
//        // check host
//        // checkServerHostConnect(serverHost);
//
//        Socket socket = null;
//        try {
//            //check port
//            socket = new Socket();
//            socket.setReceiveBufferSize(8193);
//            socket.setSoTimeout(500);
//            SocketAddress address = new InetSocketAddress(serverHost, serverPort);
//            socket.connect(address, 1000);
//        } catch (Exception ex) {
//            log.error("fail checkServerConnect", ex);
//            throw new WebServerException(ConstantCode.SERVER_CONNECT_FAIL);
//        } finally {
//            if (Objects.nonNull(socket)) {
//                try {
//                    socket.close();
//                } catch (IOException e) {
//                    log.error("fail close socket", e);
//                }
//            }
//        }
//    }


    /**
     * response exception.
     */
    public static void responseRetCodeException(HttpServletResponse response, RetCode retCode) {
        BaseResponse baseResponse = new BaseResponse(retCode);
/*        baseResponse.setMessage(ex.getMessage());
        response.setContentType("application/json;charset=UTF-8");*/
        try {
            response.getWriter().write(JsonUtils.objToString(baseResponse));
        } catch (IOException e) {
            log.error("fail responseRetCodeException", e);
        }
    }


    /**
     * check target time if within time period
     *
     * @param lastTime    target time.
     * @param validLength y:year, M:month, d:day of month, h:hour, m:minute, n:forever valid;
     *                    example1: 1d; example2:n
     */
    public static boolean isWithinPeriod(LocalDateTime lastTime, String validLength) {
        log.debug("start isWithinTime. dateTime:{} validLength:{}", lastTime, validLength);
        if ("n".equals(validLength)) {
            return true;
        }
        if (Objects.isNull(lastTime) || StringUtils.isBlank(validLength)
                || validLength.length() < 2) {
            return false;
        }
        // example: 2d (2 day)
        // 2
        String lifeStr = validLength.substring(0, validLength.length() - 1);
        if (!StringUtils.isNumeric(lifeStr)) {
            log.warn("fail isWithinTime");
            throw new RuntimeException("fail isWithinTime. validLength is error");
        }
        int lifeValue = Integer.parseInt(lifeStr);
        // d
        String lifeUnit = validLength.substring(validLength.length() - 1);
        // now is day 2, last time is day 1, 2 - 1 = 1 < 2 true
        // now is day 3, last time is day 1, 3 - 1 = 2 < 2 false, not within
        LocalDateTime now = LocalDateTime.now();
        switch (lifeUnit) {
            case "y":
                return now.getYear() - lastTime.getYear() < lifeValue;
            case "M":
                return now.getMonthValue() - lastTime.getMonthValue() < lifeValue;
            case "d":
                return now.getDayOfMonth() - lastTime.getDayOfMonth() < lifeValue;
            case "m":
                return now.getMinute() - lastTime.getMinute() < lifeValue;
            default:
                log.warn("fail isWithinTime lifeUnit:{}", lifeUnit);
                return false;
        }
    }

    /**
     * response string.
     */
    public static void responseString(HttpServletResponse response, String str) {
        log.info("start exec method[responseString], str:{}", str);
        response.setHeader("Content-Type", "application/json");
        response.setContentType("application/json;charset=UTF-8");

        BaseResponse baseResponse = new BaseResponse(ConstantCode.SYSTEM_EXCEPTION);
        if (StringUtils.isNotBlank(str)) {
            baseResponse.setMessage(str);
        }

        RetCode retCode;
        if (JsonUtils.isJson(str) && (retCode = JsonUtils.stringToObj(str, RetCode.class)) != null) {
            baseResponse = new BaseResponse(retCode);
        }

        try {
            response.getWriter().write(JsonUtils.objToString(baseResponse));
        } catch (IOException e) {
            log.error("fail responseRetCodeException", e);
        }
    }


    /**
     * get token.
     */
    public static synchronized String getToken(HttpServletRequest request) {
        String header = request.getHeader(TOKEN_HEADER_NAME);
        return WebServerTools.getToken(header);
    }

    /**
     * @param header
     * @return
     */
    public static synchronized String getToken(String header) {
        if (StringUtils.isBlank(header)) {
            log.error("not found token");
            throw new WebServerException(ConstantCode.INVALID_TOKEN);
        }

        String token = StringUtils.removeStart(header, TOKEN_START).trim();
        if (StringUtils.isBlank(token)) {
            log.error("token is empty");
            throw new WebServerException(ConstantCode.INVALID_TOKEN);
        }
        return token;
    }


    /**
     * parseHexStr2Int.
     *
     * @param str str
     * @return
     */
    public static int parseHexStr2Int(String str) {
        if (StringUtils.isBlank(str)) {
            return 0;
        }
        return Integer.parseInt(str.substring(2), 16);
    }

    /**
     * remove "0x" and last character.
     */
    public static String removeBinFirstAndLast(String contractBin, int removaLastLength) {
        if (StringUtils.isBlank(contractBin)) {
            return null;
        }
        String contractBinResult = removeFirstStr(contractBin, "0x");
        if (contractBinResult.length() > removaLastLength) {
            contractBinResult = contractBinResult.substring(0, contractBinResult.length() - removaLastLength);
        }
        return contractBinResult;
    }

    /**
     * remove fist string.
     */
    public static String removeFirstStr(String input, String target) {
        if (StringUtils.isBlank(input) || StringUtils.isBlank(target)) {
            return input;
        }
        if (input.startsWith(target)) {
            input = StringUtils.removeStart(input, target);
        }
        return input;
    }

    public static String encodedBase64Str(String input) {
        if (input == null) {
            return null;
        }
        return Base64.getEncoder().encodeToString(input.getBytes());
    }


    /**
     * @param regexEnum enum of regex type
     * @param target    target String
     * @return
     */
    public static boolean matcher(RegexPattern regexEnum, CharSequence target) {
        Pattern pattern = Pattern.compile(regexEnum.getPattern());
        Matcher matcher = pattern.matcher(target);
        return matcher.matches();
    }

    /**
     * @param calendar
     * @param millisecond
     * @return
     */
    public static Calendar CalendarPlus(Calendar calendar, int millisecond) {
        if (calendar == null) {
            calendar = Calendar.getInstance();
        }
        calendar.add(Calendar.MILLISECOND, millisecond);
        return calendar;
    }

    /**
     * @param dir
     * @param fileName
     * @return
     */
    static ThreadLocal threadLocal = ThreadLocal.withInitial(() -> new ArrayList());

    public static List<File> listFileByName(File dir, String fileName) {
        File[] fileArray = dir.listFiles((File pathname) -> {
                    if ((pathname.isFile() &&
                            fileName.equals(pathname.getName()))
                            || pathname.isDirectory()) {
                        return true;
                    }
                    return false;
                }
        );

        List<File> list = (List<File>) threadLocal.get();
        //get file list
        if (fileArray != null) {
            for (File f : fileArray) {
                if (f.isFile()) {
                    list.add(f);
                } else {
                    listFileByName(f, fileName);
                }
            }
        }
        return list;
    }

    /**
     * @param content
     * @return
     */
    public static String upperCaseFirstChar(String content) {
        if (StringUtils.isBlank(content)) return null;

        String firstChar = content.substring(0, 1);
        String otherChars = content.substring(1);
        return firstChar.toUpperCase() + otherChars;
    }


    public static void checkHttpParam(Object obj) {
        List<String> validErrorFieldName = new ArrayList<>();
        try {
            //get field with annotation
            Field[] fields = obj.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                Annotation[] annotations = field.getDeclaredAnnotations();
                long count = Arrays.stream(annotations).map(a -> a.annotationType()).filter(ac -> VALID_ANNOTATION_NAME.contains(ac)).count();
                if (count == 0) continue;

                //field value can not be null
                Object fieldValue = field.get(obj);
                if (Objects.isNull(fieldValue)) {
                    validErrorFieldName.add(field.getName());
                    continue;
                }


                //check list
                if (Collection.class.isAssignableFrom(field.getType())) {
                    Type t = field.getGenericType();
                    if (t instanceof ParameterizedType) {
                        ParameterizedType pt = (ParameterizedType) t;
                        Class clz = (Class) pt.getActualTypeArguments()[0];//得到对象Collection中实例的类型
                        if (clz.isPrimitive()) continue;

                        //get collection size
                        Class clazz = fieldValue.getClass();
                        Method sizeMethod = clazz.getDeclaredMethod("size");
                        sizeMethod.setAccessible(true);
                        int size = (Integer) sizeMethod.invoke(fieldValue);//调用Collection的size方法，得到Collection的长度

                        //foreach collection element
                        for (int i = 0; i < size; i++) {
                            Method getMethod = clazz.getDeclaredMethod("get", int.class);
                            getMethod.setAccessible(true);
                            Object element = getMethod.invoke(fieldValue, i);
                            checkHttpParam(element);
                        }
                    }
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException("check param unsuccessful", ex);
        }

        if (validErrorFieldName.size() > 0) {
            String nameStr = validErrorFieldName.stream().collect(Collectors.joining(","));
            String result = String.format("found null field:%1s", nameStr);
            throw new WebServerException(ConstantCode.PARAM_EXCEPTION.msg(result));
        }
    }


    /**
     * @param ip
     * @return
     */
    public static boolean isPing(String ip) {
        log.debug("start exec isPing [updateHost]. ip:{}", ip);
        if (ip == null) {
            log.warn("finish exec isPing [updateHost]. ip is null");
            return false;
        }
        BufferedReader in = null;
        boolean isPing = false;
        try {
            Process pro = Runtime.getRuntime().exec("ping " + ip + " -n 5 -w 5000");
            //GB2312  解决InputStreamReader乱码问题
            in = new BufferedReader(new InputStreamReader(pro.getInputStream(), "GB2312"));
            //逐行检查输出，计算类似出现=23ms TTL=62字样的次数
            int connectedCount = 0;
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
                connectedCount += getCheckResult(line);// 如果出现类似=23ms TTL=62这样的字样,出现的次数=测试次数则返回真
            }

            isPing = connectedCount == 5;
        } catch (Exception ex) {
            log.warn("fail exec isPing [updateHost].", ex);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ex) {
                    log.warn("fail exec isPing [updateHost].", ex);
                }
            }
        }
        log.info("success exec isPing [updateHost]. isPing:{}", isPing);
        return isPing;
    }

    //若line含有=18ms TTL=16字样,说明已经ping通,返回1,否則返回0.
    private static int getCheckResult(String line) {  // System.out.println("控制台输出的结果为:"+line);
        Pattern pattern = Pattern.compile("(\\d+ms)(\\s+)(TTL=\\d+)", Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(line);
        if (matcher.find()) {
            return 1;
        }
        return 0;
    }


    public static void createFileIfNotExist(File targetFile, boolean deleteOld, boolean isFile) throws IOException {
        Objects.requireNonNull(targetFile, "fail to create file:targetFile is null");
        File parentFile = targetFile.getParentFile();
        if (!parentFile.exists())
            parentFile.mkdirs();

        if (deleteOld)
            targetFile.deleteOnExit();

        if (targetFile.exists())
            return;

        if (isFile) {
            targetFile.createNewFile();
        } else {
            targetFile.mkdir();
        }
    }


    /**
     * @param chars
     * @return
     */
    public static byte[] charsToBytes(char[] chars) {
        final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars));
        return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
    }

    /**
     * @param bytes
     * @return
     */
    public static char[] bytesToChars(byte[] bytes) {
        final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes));
        return Arrays.copyOf(charBuffer.array(), charBuffer.limit());
    }


    /**
     * @param file
     * @return
     */
    public static String convertFileToBase64(File file) {
        if (null == file)
            return null;

        if (!file.isFile())
            return null;

        try {
            byte[] logoFileByteArray = FileUtils.readFileToByteArray(file);
            return Base64.getEncoder().encodeToString(logoFileByteArray);
        } catch (IOException e) {
            log.error("fail exec mentod convertFileToBase64", e);
        }
        return null;
    }

}
